Java IO浅析

Java中的IO操作

Java总的来说有三类IO,效率不高,操作简单的BIO(blocking IO),非阻塞的NIO(New IO),和异步非阻塞IO,也就是升级版的NIO(Asynchronous I/O).

IO分类

IO分类

在学习这三类IO前,需要了解什么是阻塞.什么是异步.两个的含义有什么区别.

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个调用*时,在没有得到结果之前,该 *调用 就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,但是没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态来通知调用者。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会重启线程。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

BIO

BIO过程就如同名字一样,是一个阻塞的IO,服务端通常为每一个客户端都建立一个独立的线程来通过调用accept()来监听客户端消息.如果想处理多个客户端请求则服务端需要建立等同数量的线程来处理这些消息,这就是普遍的一请求一应答的模型.处理完成后返回应答给客户端后销毁线程,因为线程是一个昂贵的资源,这样重复的新建线程,销毁线程,很浪费处理器资源,所以使用BIO同时能够尽可能的少创建线程,就可以用到线程池的方式实现,来达到服务端创建线程数远远小于客户端数的目的,但这种方法只是伪异步IO.

在处理链接数量少的情况下,BIO的效率还不错,并且主要逻辑模型清晰明了,代码简单.但是在上万的链接的情况下,BIO处理起来就非常吃紧了.

NIO

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

NIO特性和NIO与传统IO的区别

  • 传统IO(BIO)是一种阻塞IO模型,而NIO是非阻塞的IO模型,区别为当线程读取数据的时候,非阻塞IO可以不用等,而阻塞IO需要一直等待IO完成后才能继续.
  • IO面向流,而NIO面向缓冲区.
  • 通道(channel) NIO通过通道进行数据读写.通道是双向的,而传统的IO是单向的.通道链接的都是Buffer,所以通道可以异步的读写.
  • 选择器(Selectors) NIO拥有选择器,而IO没有.选择器的作用就是用来使用单个线程来处理多个通道(NIO面向buffer,通道只与buffer交互).

Selector图解

AIO

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。

BIO的流操作

根据上面的IO分类图可以看到IO流按照流的类型可以分为两类,一类是字节流,一类是字符流

两者之间的区别在于

操作单位不同,字节流以字节为单位进行数据传输,而字符流是以字符为单位进行传输
处理元素不同,字节流可处理所有类型数据,但字符流只可以处理以字符类型的数据,也就是说字符流只可处理纯文本数据

输入输出流

输入输出按照字面理解,就是流中的输入和输出

流类型 输入 输出
字节流 InputStream OutputStream
字符流 Reader Writer

字节流

输入字节流 InputStream

InputStream

inputStream作为抽象类,必须依靠子类实现具体的操作.在抽象类中定义了如下几个方法:

  1. 三个重载的read方法,用来读取数据,其中必须在子类中实现抽象的read方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract int read() throws IOException;

// 定义b.length读取范围
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
// 使用子类中的read进行数据读取
int c = read();
//....
}
  1. skip(long n) 方法,用来掉过并丢弃n个字节的数据,并返回被丢弃的数据
  2. available()方法,用来返回输入流中可以读取的字节数,子类需要单独实现该方法,否则会返回0;
  3. void close() 方法,子类实现,用来关闭流
  4. synchronized void mark(int readlimit)方法,用来标记输入流的当前位置,同样由子类来具体实现
  5. synchronized void reset()方法,用来返回输入流最后一次调用mark方法的位置

来看看几种不同的InputStream:

  1. FileInputStream 把一个文件作为InputStream,实现对文件的读取操作
  2. ByteArrayInputStream 把内存中的一个缓冲区作为InputStream使用
  3. StringBufferInputStream 把一个String对象作为InputStream
  4. PipedInputStream 实现了pipe的概念,主要在线程中使用
  5. SequenceInputStream把多个InputStream合并为一个InputStream

输出字节流 outputStream

  1. OutputStream提供了3个重载的write方法来做数据的输出
1
2
3
4
5
6
// 将参数b中的字节写到输出流 
public void write(byte b[ ])
// 将参数b的从偏移量off开始的len个字节写到输出流
public void write(byte b[ ], int off, int len)
// 先将int转换为byte类型,把低字节写入到输出流中
public abstract void write(int b)
  1. public void flush() 将数据缓冲区中数据全部输出,并清空缓冲区。
  2. public void close() 关闭输出流并释放与流相关的系统资源。

字符流

字符输入流 Reader

字符输入流和字节流相似,同样定义了read相关方法,但是不同的点在于Reader操作char而不是byte,并且在声明Reader时,将自身作为一个对象,在相关操作上使用synchronized进行同步操作.

1
2
3
4
5
6
7
protected Reader() {
this.lock = this;
}
// 实现方法
public int read(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
// ...

同理字符输出流 Writer

如何使用BIO流

  1. 首先确定是输入还是输出
  2. 其次确认对象是否为纯文本,如果是纯文本可以选择 字符流的 ReaderWirter ,否则需要使用字节流的 inputStreamoutputStream
  3. 然后确定是否要通过流转换来达到增加处理效率的目的,如果需要则使用 InputStreamReader 等进行转换
  4. 最后,需要确认是否需要使用buffer缓冲来提高效率
  • inputStream 字节输入流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void inputStreamTest(){
String filePath = "/Users/gaoguoxing/Work/temp/attachment/20200403/65bf6a2b0dbf3049dc08e800c2ac617385bb.xml";
try (InputStream in
= new FileInputStream(filePath)
) {
// 获取文件IO流
byte[] content = new byte[100];
StringBuffer bf = new StringBuffer();
// 没有返回 -1
while (true) {
if (in.read(content) < 0) {
break;
}
bf.append(new String(content));
}
System.out.println(bf.toString());
} catch (IOException e) {

}
}
  • outputStream 字节输出流
1
2
3
4
5
6
7
8
public void outputStreamTest(){
String content = "this is my content";
try (OutputStream out = new FileOutputStream("copy.txt")) {
out.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
  • Reader 字符输入流
1
2
3
4
5
6
7
8
9
10
11
public void readerTest(){
String filePath = "/Users/gaoguoxing/Work/temp/attachment/20200403/65bf6a2b0dbf3049dc08e800c2ac617385bb.xml";
String s;
try(BufferedReader in = new BufferedReader(new FileReader(filePath))) {
while ((s = in.readLine()) != null){
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
  • Writer 字符输出流
1
2
3
4
5
6
7
8
public void writerTest(){
String content = "this is my content";
try (FileWriter out = new FileWriter("copy.txt");) {
out.write(content + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}

NIO操作

在NIO特性一章中提到了NIO是面向Buffer的,双向的数据处理形式。因为NIO分为buffer缓冲区和Channel管道,NIO使用管道操作缓冲区,可以说Channel不与数据打交道,它只负责运输数据。更可以抽象的简单理解为Channel管道为铁路,buffer缓冲区为火车(运载着货物),火车可以去,同样也可以回(双向的)。

Buffer 缓冲区

Buffer是具体的原始类型的容器,Buffer是一个线性的、有限的原始类型元素的集合。Buffer中有三个必要的属性:capacity、limit、position

  • capacity:buffer中包含的元素数量
  • limit:buffer中的limit是缓冲区里的数据的总数
  • position:buffer中position是下一个即将被读写的元素

每个实现子类都需要实现两种方法:get和put,两个是相对的操作,也就是理解为读写数据,每次操作时,都会从buffer中的当前position开始,增长transferred个数量的元素,这里的transferred就是get和put的元素数量。如果使用get操作,超出了limit,那么会出现 BufferUnderflowException ,相反如果使用get超出limit,就会出现 BufferOverflowException,这两种情况下,数据都不会被改变。

Buffer中提供了clear()、 filp()、 rewind()方法用来访问Buffer中的position, limit, 和capacity的值。

  • clear() 清空读缓冲区中的内容,之后可以使用put写数据,将limit设置为capacity,将position设置为0
  • filp() 切换成读模式,之后可以使用 get 读数据,将limlit设置为当前position,再将position设置为0
  • rewind() 可重复读缓冲区的内容

下面通过几个实例来看Buffer相关的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 分配capacity大小
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 初始时4个核心变量的值
System.out.println("limit:"+byteBuffer.limit()); // 1024
System.out.println("position:"+byteBuffer.position()); // 0
System.out.println("capacity:"+byteBuffer.capacity()); // 1024
System.out.println("mark:" + byteBuffer.mark());
//mark:java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]

byteBuffer.put("hello world".getBytes());
// 调用mark方法会返回this也就会输出当前buffer
System.out.println("mark:" + byteBuffer.mark());
// mark:java.nio.HeapByteBuffer[pos=11 lim=1024 cap=1024]
// 在执行put操作后,当前位置发生了变化

// 切换成读模式
byteBuffer.flip();
System.out.println("mark:" + byteBuffer.mark());
// mark:java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
// 切换成读模式,limit设置为原来的当前位置,当前位置设置为0
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
// 这样使用get读的时候,只能获取到 0~limit 之间的内容
System.out.println(new String(bytes));

// 清空,再次可以向buffer中写数据
byteBuffer.clear();
byteBuffer.put("HELLO WORLD".getBytes());
//mark:java.nio.HeapByteBuffer[pos=11 lim=1024 cap=1024]
System.out.println("mark:" + byteBuffer.mark());

// 切换成读模式
byteBuffer.flip();
//mark:java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
System.out.println("mark:" + byteBuffer.mark());
bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(new String(bytes));

// 重复读
System.out.println("mark:" + byteBuffer.mark());
//mark:java.nio.HeapByteBuffer[pos=11 lim=11 cap=1024]
// 使用get之后,pos变为了limit,在读的话会出现异常,
// 如果再读,只能使用rewind方法
byteBuffer.rewind();
//mark:java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
System.out.println("mark:" + byteBuffer.mark());
bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(new String(bytes));

Channel 管道

Channel是IO操作的核心,Channel表示与一些硬件设备,文件,网络等实体的开放连接,能够进行多个不同的IO操作(读与写).

Channel有开关的状态,只要channel创建,则channel的状态就是open的,如果chennel一旦被关闭,那么如果再有后续调用channel的io操作,都会出现异常.(类似jdbc的connection),以防万一,可以调用isopen()方法检测是否被关闭了

再NIO中几个重要的Channel实现类:

  • FileChannel: 用于文件的数据读写
  • DatagramChannel: 用于UDP的数据读写
  • SocketChannel: 用于TCP的数据读写,一般是客户端实现
  • ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现

用FileChannel来演示创建和传输的过程.

创建channel

有两种方式创建channel,一种是使用file或者fileStream创建cannel,另一种使用FileChannel的静态方法创建

1
2
3
4
5
6
7
8
// 1. 使用randomAccessFile 创建channel
RandomAccessFile randomAccessFile = new RandomAccessFile("a.txt","rw");
FileChannel in = randomAccessFile.getChannel();
// 2. 使用静态方法创建
FileChannel out = FileChannel.open(Paths.get("b.txt"), StandardOpenOption.WRITE);
// 3. 通过FileInputStream 创建channel
FileInputStream inputStream = new FileInputStream("xxx.txt");
inputStream.getChannel();

数据读写

使用channel进行数据读写,类似普通的BIO使用buffer.但是需要注意的是,每次都需要将buffer打开读模式,再读,读完后使用clear清空buffer

1
2
3
4
5
6
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
while(in.read(readBuffer) != -1){
readBuffer.flip();
out.write(readBuffer);
readBuffer.clear();
}

还可以直接使用通道的transferTo直接复制到另外一个通道中,完成复制

1
2
// 使用通道的transferTo
in.transferTo(0,in.size(),out);

关闭资源

最后需要手动将channel关掉(必须)

1
2
in.close();
out.close();

另外这时候可以使用 try-with-resource 进行简化,整体过程可以参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try (
// 创建资源
RandomAccessFile randomAccessFile = new RandomAccessFile("a.txt", "rw");
FileChannel in = randomAccessFile.getChannel();
FileChannel out = FileChannel.open(Paths.get("b.txt"), StandardOpenOption.WRITE);
) {
// 设置buffer
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
// 将通道中的数据放到缓冲区
while (in.read(readBuffer) != -1) {
// 切换成读模式
readBuffer.flip();
// 向通道内写入buffer
out.write(readBuffer);
// 清空本次的buffer
readBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
-------------本文结束感谢您的阅读-------------

本文标题:Java IO浅析

文章作者:NanYin

发布时间:2020年04月05日 - 00:04

最后更新:2020年04月09日 - 08:04

原始链接:https://nanyiniu.github.io/2020/04/05/Jav%20IO%E6%B5%85%E6%9E%90/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。